<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第五章：Agents模块</title>
    <link rel="stylesheet" href="../shared_styles.css">
</head>
<body>
    <div class="container">
        <h1>第五章：Agents模块</h1>

        <p>在Langchain中，Agent是一个核心概念，它代表了一个能够观察环境、思考决策并采取行动的智能体。与Chain不同，Agent的行为不是预先确定的，而是根据大语言模型（LLM）的判断动态决定的。这使得Agent能够处理更复杂、更具不确定性的任务。</p>

        <h2 id="5-1">5.1 什么是Agent？</h2>
        <p>Agent的核心思想是利用LLM作为“大脑”或“推理引擎”。LLM接收用户的输入以及一系列可用的工具（Tools），然后决定下一步应该采取什么行动。这个行动可能是调用一个工具，也可能是直接向用户返回一个答案。</p>
        <p>Agent的运行流程，也称为“推理循环”或“决策循环”，通常如下：</p>
        <ol>
            <li><strong>接收用户输入 (Input)</strong>: Agent接收到用户的原始请求或问题。</li>
            <li><strong>构建提示 (Prompt Engineering)</strong>: 这是Agent运作的关键。用户的输入、可用的工具列表（及其描述）、历史交互记录（如果有的话，例如在对话型Agent中），以及特定的指令格式（例如ReAct框架中的“Thought, Action, Action Input, Observation”结构）被组合成一个精心设计的提示，然后发送给LLM。这个提示指导LLM如何思考和回应。</li>
            <li><strong>LLM进行思考和决策 (Reasoning & Decision Making)</strong>: LLM处理这个提示，进行“思考”。这个思考过程旨在分析问题，选择最合适的工具（如果需要），或者判断是否可以直接回答。LLM会以预定义的格式输出其思考过程和决策。例如，在ReAct框架下，LLM可能会输出：
                <ul>
                    <li><strong>Thought (思考)</strong>: LLM的内部推理过程，解释它为什么选择某个行动。</li>
                    <li><strong>Action (行动)</strong>: LLM决定采取的具体行动，通常是调用一个工具的名称。</li>
                    <li><strong>Action Input (行动输入)</strong>: LLM为选定工具准备的输入参数。</li>
                </ul>
                如果LLM认为它可以直接回答，它可能会输出一个特殊的标记，如“Final Answer”。
            </li>
            <li><strong>解析LLM输出 (Output Parsing)</strong>: Agent框架（如Langchain中的<code>AgentExecutor</code>）会解析LLM的结构化输出。它提取出“Action”和“Action Input”。</li>
            <li><strong>执行行动 (Tool Execution)</strong>:
                <ul>
                    <li>如果LLM决定调用工具，<code>AgentExecutor</code>会根据提取的“Action”（工具名称）和“Action Input”（工具参数）来执行相应的工具函数。</li>
                    <li>执行选定的工具，并获取工具的输出结果，这个结果被称为“Observation (观察)”。</li>
                </ul>
            </li>
            <li><strong>反馈与迭代 (Feedback & Iteration)</strong>:
                <ul>
                    <li>工具的输出（Observation）会连同之前的交互历史一起，再次被整合到新的提示中，反馈给LLM。</li>
                    <li>LLM根据这个新的信息（包含用户原始输入、历史行动、以及最新的工具观察结果）再次进行思考和决策（返回到第3步）。这个循环会持续进行。</li>
                </ul>
            </li>
            <li><strong>任务完成与最终输出 (Task Completion & Final Output)</strong>:
                <ul>
                    <li>循环会持续，直到LLM在其输出中表明任务已完成（例如，通过输出“Final Answer: [最终答案]”），或者达到了预设的停止条件（如最大迭代次数、超时等）。</li>
                    <li>一旦LLM提供了最终答案，<code>AgentExecutor</code>会将其提取出来并返回给用户。</li>
                </ul>
            </li>
        </ol>
        <p>这种动态的、基于LLM决策和工具使用的迭代执行流程，赋予了Agent极大的灵活性和强大的问题解决能力。其核心“智能”来源于LLM的推理能力，而工具则扩展了LLM可以交互和影响的世界的边界。</p>

        <h2 id="5-2">5.2 Agent的核心组件</h2>
        <p>一个典型的Langchain Agent由以下几个核心组件构成：</p>
        <ul>
            <li><strong>Agent (Agent Class)</strong>: 这是Agent的核心逻辑所在。它是一个类，负责接收用户输入和历史交互，然后调用LLM来决定下一步的行动。Langchain内置了多种Agent类型，例如<code>ZeroShotAgent</code>, <code>ReActAgent</code>, <code>SelfAskWithSearchAgent</code>等，它们基于不同的提示工程和策略。</li>
            <li><strong>Tools (工具)</strong>: 工具是Agent可以执行的特定功能的函数或模块。例如，一个工具可以是搜索引擎、计算器、数据库查询接口、API调用函数等。Agent通过调用这些工具来获取信息或执行操作，从而完成更复杂的任务。每个工具都需要一个清晰的名称和描述，以便LLM能够理解何时以及如何使用它。</li>
            <li><strong>Toolkits (工具包)</strong>: 工具包是一组为了特定目的而预先封装好的工具集合。例如，<code>SQLDatabaseToolkit</code> 包含了与SQL数据库交互的多种工具。使用工具包可以方便地为Agent配备一套相关的能力。</li>
            <li><strong>AgentExecutor</strong>: 这是Agent的运行环境。它负责实际执行Agent的决策循环：接收Agent的行动指令，调用相应的工具，获取工具的输出，并将输出反馈给Agent，直到Agent完成任务。它还处理一些细节，如最大迭代次数、错误处理等。</li>
        </ul>

        <h2 id="5-3">5.3 Agent的类型</h2>
        <p>Langchain提供了多种预设的Agent类型，它们基于不同的策略和LLM的交互方式。以下是一些常见的Agent类型：</p>
        <ul>
            <li><strong>Zero-shot ReAct (<code>zero-shot-react-description</code>)</strong>:
                <ul>
                    <li><strong>原理</strong>: ReAct框架结合了推理（Reasoning）和行动（Acting）。Agent通过LLM生成一系列的“思考（Thought）”和“行动（Action）”。“思考”是LLM的推理过程，用于分析当前情况并决定下一步。“行动”是具体要执行的操作，通常是调用一个工具。</li>
                    <li><strong>特点</strong>: 这种Agent仅根据工具的描述来决定使用哪个工具，因此称为“Zero-shot”。它非常通用，但对LLM的推理能力要求较高。</li>
                </ul>
            </li>
            <li><strong>Self-ask with search (<code>self-ask-with-search</code>)</strong>:
                <ul>
                    <li><strong>原理</strong>: 这种Agent专门设计用于需要通过搜索来回答问题的场景。它会首先尝试直接回答问题，如果不行，它会自己提出一个后续问题（self-ask），然后利用搜索工具（通常是唯一的工具）来寻找这个后续问题的答案。这个过程会迭代进行，直到原始问题得到解答。</li>
                    <li><strong>特点</strong>: 结构相对简单，目标明确，非常适合问答和信息检索类任务。</li>
                </ul>
            </li>
            <li><strong>Conversational ReAct (<code>conversational-react-description</code>)</strong>:
                <ul>
                    <li><strong>原理</strong>: 类似于Zero-shot ReAct，但设计用于对话场景。它能够利用对话历史（Memory）来更好地理解上下文，并做出更连贯的决策。</li>
                    <li><strong>特点</strong>: 适合构建聊天机器人或需要多轮交互的Agent。</li>
                </ul>
            </li>
            <li><strong>Plan and execute</strong>:
                <ul>
                    <li><strong>原理</strong>: 这种Agent首先制定一个计划（一系列步骤），然后逐个执行这些步骤。计划本身可以由LLM生成，也可以是预定义的。</li>
                    <li><strong>特点</strong>: 对于需要多步骤、有明确顺序的任务非常有效。</li>
                </ul>
            </li>
        </ul>
        <p>选择哪种Agent类型取决于具体的任务需求、LLM的能力以及可用的工具。</p>

        <h2 id="5-4">5.4 使用Qwen模型创建Agent示例</h2>
        <p>下面我们将演示如何使用通义千问（Qwen）模型来创建不同类型的Agent。</p>
        <p>首先，进行一些通用的设置，包括导入模块和初始化LLM。请确保您已设置环境变量<code>DASHSCOPE_API_KEY</code>。</p>
        <pre><code class="language-python">
import os
from datetime import date
from langchain_community.llms import Tongyi
from langchain.agents import AgentType, initialize_agent, Tool
from langchain.memory import ConversationBufferMemory
from langchain_community.utilities import SerpAPIWrapper # 用于Self-ask with search的示例

# 确保设置了您的通义千问API密钥
# os.environ["DASHSCOPE_API_KEY"] = "YOUR_DASHSCOPE_API_KEY"
# 如果使用SerpAPI，也需要设置其API Key
# os.environ["SERPAPI_API_KEY"] = "YOUR_SERPAPI_API_KEY"

# 1. 初始化Qwen模型
# 替换为你实际使用的Qwen模型名称，例如 qwen-turbo, qwen-plus, qwen-max 等
try:
    llm = Tongyi(model_name="qwen-turbo")
    print("Qwen LLM 初始化成功！")
except Exception as e:
    print(f"初始化Qwen模型失败，请检查API Key和网络连接: {e}")
    llm = None # 设置为None，以便后续代码可以安全检查

# 通用工具定义
def get_current_date(input_text: str) -> str:
    """当需要知道当前日期时，调用此工具。输入应为一个空字符串。"""
    print(f"调用 get_current_date 工具，输入: '{input_text}'")
    return f"今天的日期是 {date.today().strftime('%Y年%m月%d日')}。"

date_tool = Tool(
    name="GetCurrentDate",
    func=get_current_date,
    description="非常有用，当需要获取当前日期的时候。输入应该是一个空字符串。"
)

def simple_calculator(expression: str) -> str:
    """当需要进行简单的数学计算时调用此工具。输入应该是一个数学表达式字符串，例如 '2+2' 或 '10*5'。"""
    print(f"调用 simple_calculator 工具，输入: '{expression}'")
    try:
        result = eval(expression) # 注意：eval有安全风险，实际应用中应使用更安全的解析器
        return f"计算结果: {expression} = {result}"
    except Exception as e:
        return f"计算错误: {e}"

calculator_tool = Tool(
    name="SimpleCalculator",
    func=simple_calculator,
    description="非常有用，当需要进行简单的加减乘除数学运算时。输入应该是一个数学表达式字符串，例如 '2+2' 或 '10*5'。"
)
        </code></pre>

        <h4 id="5-4-1">5.4.1 Zero-shot ReAct Agent 示例</h4>
        <p>这是最通用的Agent类型之一，它根据工具的描述来决定如何行动。</p>
        <pre><code class="language-python">
if llm:
    print("\n--- Zero-shot ReAct Agent 示例 ---")
    tools_for_react = [date_tool, calculator_tool]
    try:
        zero_shot_agent = initialize_agent(
            tools_for_react,
            llm,
            agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
            verbose=True,
            handle_parsing_errors=True
        )

        print("\n--- 任务1 (Zero-shot): 今天是几号？ ---")
        response1 = zero_shot_agent.invoke({"input": "你好，请问今天是几号？"})
        print(f"Agent最终回答: {response1['output']}")

        print("\n--- 任务2 (Zero-shot): 3乘以4加7等于多少？ ---")
        response2 = zero_shot_agent.invoke({"input": "请帮我计算一下 3 * 4 + 7 等于多少？"})
        print(f"Agent最终回答: {response2['output']}")

    except Exception as e:
        print(f"运行 Zero-shot ReAct Agent 时发生错误: {e}")
else:
    print("LLM未能成功初始化，Zero-shot ReAct Agent 无法运行。")
        </code></pre>
        <p><strong>Zero-shot ReAct Agent 的思考过程示例 (verbose=True):</strong></p>
        <p>当你运行上述代码并提出问题 "今天是几号？" 时，如果<code>verbose=True</code>，你可能会看到类似以下的输出：</p>
        <pre><code>
> Entering new AgentExecutor chain...
Thought: The user is asking for today's date. I have a tool called GetCurrentDate that can provide this information.
Action: GetCurrentDate
Action Input: ""
调用 get_current_date 工具，输入: ''
Observation: 今天的日期是 2024年07月16日。
Thought: I have the current date. I can now answer the user's question.
Final Answer: 今天的日期是 2024年07月16日。

> Finished chain.
Agent最终回答: 今天的日期是 2024年07月16日。
        </code></pre>

        <h4 id="5-4-2">5.4.2 Self-ask with search Agent 示例</h4>
        <p>这种Agent适用于需要通过搜索来分解和回答复杂问题的场景。它通常只使用一个名为 "Intermediate Answer" (或类似名称，取决于LLM) 的搜索工具。</p>
        <p>为了演示，我们将使用<code>SerpAPIWrapper</code>作为搜索工具。您需要一个SerpAPI的API密钥。如果不想使用实际的API，可以创建一个模拟的搜索工具函数。</p>
        <pre><code class="language-python">
if llm:
    print("\n--- Self-ask with search Agent 示例 ---")
    # 尝试初始化SerpAPI，如果失败则使用模拟工具
    try:
        if not os.environ.get("SERPAPI_API_KEY"):
            raise ValueError("SERPAPI_API_KEY 未设置，将使用模拟搜索工具。")
        search_tool_serp = SerpAPIWrapper()
        tools_for_self_ask = [
            Tool(
                name="Intermediate Answer", # 这个名称对SelfAsk Agent很重要
                func=search_tool_serp.run,
                description="useful for when you need to ask with search. Input should be a search query."
            )
        ]
        print("使用 SerpAPI 作为搜索工具。")
    except Exception as e_serp:
        print(f"初始化SerpAPI失败: {e_serp}. 将使用模拟搜索工具。")
        def mock_search(query: str) -> str:
            print(f"模拟搜索工具被调用，查询: '{query}'")
            if "capital of france" in query.lower():
                return "Paris is the capital of France."
            elif "tallest mountain" in query.lower():
                return "Mount Everest is the tallest mountain in the world."
            elif "author of 'The Three-Body Problem'" in query.lower():
                return "The author of 'The Three-Body Problem' is Liu Cixin."
            return "Sorry, I couldn't find an answer for that in my mock database."

        tools_for_self_ask = [
            Tool(
                name="Intermediate Answer",
                func=mock_search,
                description="useful for when you need to ask with search. Input should be a search query."
            )
        ]

    try:
        self_ask_agent = initialize_agent(
            tools_for_self_ask,
            llm,
            agent=AgentType.SELF_ASK_WITH_SEARCH,
            verbose=True,
            handle_parsing_errors=True
        )

        print("\n--- 任务1 (Self-ask): 法国的首都是哪里？ ---")
        # 注意：Self-ask agent对问题的措辞可能比较敏感，确保问题能够引导其进行分解
        response_sa1 = self_ask_agent.invoke({"input": "法国的首都是哪里？"})
        print(f"Agent最终回答: {response_sa1['output']}")

        print("\n--- 任务2 (Self-ask): 《三体》的作者是谁？ ---")
        response_sa2 = self_ask_agent.invoke({"input": "《三体》这本书的作者是谁？"})
        print(f"Agent最终回答: {response_sa2['output']}")

    except Exception as e:
        print(f"运行 Self-ask with search Agent 时发生错误: {e}")
else:
    print("LLM未能成功初始化，Self-ask with search Agent 无法运行。")

        </code></pre>
        <p><strong>Self-ask with search Agent 的思考过程示例 (verbose=True):</strong></p>
        <p>对于问题 "《三体》这本书的作者是谁？"，输出可能如下：</p>
        <pre><code>
> Entering new AgentExecutor chain...
Thought:
The user is asking for the author of the book "The Three-Body Problem".
I should use the search tool to find this information.
Follow up: Who is the author of 'The Three-Body Problem'?
Action: Intermediate Answer
Action Input: Who is the author of 'The Three-Body Problem'?
模拟搜索工具被调用，查询: 'Who is the author of 'The Three-Body Problem'?'
Observation: The author of 'The Three-Body Problem' is Liu Cixin.
Thought: I have found the answer.
So the final answer is: Liu Cixin

> Finished chain.
Agent最终回答: Liu Cixin
        </code></pre>

        <h4 id="5-4-3">5.4.3 Conversational ReAct Agent 示例</h4>
        <p>这种Agent结合了ReAct的推理能力和对话记忆（Memory），使其能够进行多轮、有上下文的交互。</p>
        <pre><code class="language-python">
if llm:
    print("\n--- Conversational ReAct Agent 示例 ---")
    tools_for_conv = [date_tool, calculator_tool] # 可以复用之前的工具

    # 初始化对话记忆
    memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

    try:
        conversational_agent = initialize_agent(
            tools_for_conv,
            llm,
            agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
            verbose=True,
            memory=memory,
            handle_parsing_errors=True,
            # 对于Qwen等模型，可能需要调整agent_kwargs以优化提示
            # agent_kwargs={
            #     "system_message": "You are a helpful AI assistant. Respond to the human as helpfully and accurately as possible.",
            #     "human_message": "{input}\n\n{agent_scratchpad}" # 确保 agent_scratchpad 在正确的位置
            # }
        )

        print("\n--- 对话1 (Conversational): 你好！ ---")
        response_c1 = conversational_agent.invoke({"input": "你好！我叫小明。"})
        print(f"Agent回复: {response_c1['output']}")

        print("\n--- 对话2 (Conversational): 今天是几号？ ---")
        response_c2 = conversational_agent.invoke({"input": "请问今天是几号？"})
        print(f"Agent回复: {response_c2['output']}")

        print("\n--- 对话3 (Conversational): 5乘以12等于多少？ ---")
        response_c3 = conversational_agent.invoke({"input": "那么，5乘以12等于多少？"})
        print(f"Agent回复: {response_c3['output']}")
        
        print("\n--- 对话4 (Conversational): 还记得我叫什么名字吗？ ---")
        response_c4 = conversational_agent.invoke({"input": "你还记得我叫什么名字吗？"})
        print(f"Agent回复: {response_c4['output']}")
        
        # 查看记忆内容
        # print("\n--- 当前对话记忆 ---")
        # print(memory.load_memory_variables({}))


    except Exception as e:
        print(f"运行 Conversational ReAct Agent 时发生错误: {e}")
else:
    print("LLM未能成功初始化，Conversational ReAct Agent 无法运行。")

        </code></pre>
        <p><strong>Conversational ReAct Agent 的思考过程示例 (verbose=True):</strong></p>
        <p>在多轮对话中，Agent会利用<code>memory</code>中的历史信息。例如，在问完日期后，再问计算问题，Agent的思考过程会包含历史对话。</p>
        <p>当问 "你还记得我叫什么名字吗？" 时，Agent应该能从记忆中找到 "我叫小明。"</p>
        <pre><code>
> Entering new AgentExecutor chain...
Thought: The user is asking if I remember their name. I should check the chat history. The chat history indicates the user introduced themselves as '小明'.
Final Answer: 是的，我记得您叫小明。

> Finished chain.
Agent回复: 是的，我记得您叫小明。
        </code></pre>
        <p><strong>关于代码和Agent行为的总体说明：</strong></p>
        <ul>
            <li><strong>API密钥</strong>: 请确保在运行代码前，正确设置了<code>DASHSCOPE_API_KEY</code>（以及可选的<code>SERPAPI_API_KEY</code>）环境变量。</li>
            <li><strong>工具描述</strong>: 工具的<code>description</code>字段对于Agent至关重要。LLM依赖这些描述来理解工具的功能以及何时使用它们。描述应清晰、准确、具体。</li>
            <li><strong><code>verbose=True</code></strong>: 设置此参数可以让你看到Agent的详细思考过程（Thought, Action, Observation），这对于调试和理解Agent行为非常有帮助。</li>
            <li><strong><code>handle_parsing_errors=True</code></strong>: 这个参数可以帮助Agent处理LLM输出不完全符合预期格式的情况，增加Agent的鲁棒性。有时LLM的输出可能略有偏差，此参数会尝试进行纠正或向LLM请求重新格式化。</li>
            <li><strong>LLM的适应性</strong>: 不同的LLM对特定Agent类型的提示（Prompt）和工作方式的适应性可能不同。Qwen系列模型通常具有较强的通用指令遵循能力，适合ReAct类型的Agent。但有时，为了达到最佳效果，可能需要微调Agent的内部提示模板（通过<code>agent_kwargs</code>）。</li>
            <li><strong>Self-ask with search的工具名称</strong>: 对于<code>SELF_ASK_WITH_SEARCH</code>类型的Agent，它期望的搜索工具名称通常是 "Intermediate Answer"。如果LLM是其他语言（比如中文），它可能期望本地化的工具名，这可能需要调整或在初始化时指定。</li>
            <li><strong>Conversational Agent的Memory</strong>: <code>ConversationBufferMemory</code>是最简单的一种记忆类型，它会存储整个对话历史。对于非常长的对话，这可能会导致上下文窗口超出限制。Langchain提供了其他更高级的Memory类型，如<code>ConversationSummaryMemory</code>或<code>ConversationKGMemory</code>来处理长对话。</li>
        </ul>


        <h2 id="5-5">5.5 Agent的高级主题</h2>
        <ul>
            <li><strong>自定义Agent</strong>: 虽然Langchain提供了多种预置Agent，但有时你可能需要根据特定需求创建自定义Agent。这通常涉及到编写自定义的LLM Prompt模板，以及解析LLM输出的逻辑。</li>
            <li><strong>Agent的错误处理</strong>: Agent在执行过程中可能会遇到各种错误，例如工具调用失败、LLM输出格式错误等。<code>AgentExecutor</code>提供了一些参数（如<code>handle_parsing_errors</code>）来处理这些情况，但复杂的场景可能需要更精细的错误处理机制。</li>
            <li><strong>Agent与Memory的结合</strong>: 为了让Agent能够记住之前的交互内容，可以将其与Memory模块结合起来。这对于构建能够进行连贯对话的Agent至关重要，如<code>CONVERSATIONAL_REACT_DESCRIPTION</code>示例所示。</li>
            <li><strong>Agent的安全性</strong>: 由于Agent可以执行代码或调用外部API，因此必须非常小心地设计和限制其能力，以防止潜在的安全风险。例如，在我们的<code>simple_calculator</code>工具中使用了<code>eval()</code>，这在生产环境中通常是不推荐的，除非有严格的输入校验和沙箱环境。应优先选择更安全的表达式求值库。</li>
            <li><strong>工具的异步执行</strong>: 对于耗时较长的工具，可以考虑使用异步版本的Agent和工具来提高性能。</li>
        </ul>

        <h2 id="summary">总结</h2>
        <p>Agent是Langchain中非常强大的一个模块，它使得LLM能够超越简单的文本生成，具备了与外部世界交互、执行复杂任务的能力。通过合理地设计工具和选择合适的Agent类型，你可以构建出各种智能应用。然而，Agent的设计和调试也更具挑战性，需要对LLM的行为、提示工程以及工具的交互有深入的理解。多尝试不同的Agent类型和工具组合，并仔细观察其<code>verbose</code>输出，是掌握Agent的关键。</p>

        <div class="navigation">
            <a href="../langchain_tutorial_outline.html">返回目录</a>
            <a href="../chapter4/chapter4_chains.html">上一章：Chains模块</a>
            <a href="../chapter6/chapter6_memory.html">下一章：Memory模块</a>
        </div>
    </div>
</body>
</html>
